// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:io';

import 'package:meta/meta.dart';

import '../config/config.dart';
import '../config/experiments.dart';
import '../elements/elements.dart';
import '../logging/logging.dart';
import '../util/string_util.dart';
import 'c_generator.dart';
import 'resolver.dart';
import 'visitor.dart';

// Import prefixes.
const _jni = 'jni';
const _ffi = 'ffi';

// package:jni types.
const _jType = '$_jni.JObjType';
const _jPointer = '$_jni.JObjectPtr';
const _jReference = '$_jni.JReference';
const _jGlobalReference = '$_jni.JGlobalReference';
const _jArray = '$_jni.JArray';
const _jObject = '$_jni.JObject';
const _jResult = '$_jni.JniResult';

// package:ffi types.
const _voidPointer = '$_ffi.Pointer<$_ffi.Void>';

// Prefixes and suffixes.
const _typeParamPrefix = '\$';

// Misc.
const _protectedExtension = 'ProtectedJniExtensions';
const _classRef = '_class';
const _lookup = 'jniLookup';

/// Used for C bindings.
const _selfPointer = 'reference.pointer';

/// Used for Dart-only bindings.
const _self = 'this';

// Docs.
const _releaseInstruction =
    '  /// The returned object must be released after use, '
    'by calling the [release] method.';

extension on Iterable<String> {
  /// Similar to [join] but adds the [separator] to the end as well.
  String delimited([String separator = '']) {
    return map((e) => '$e$separator').join();
  }
}

/// Encloses [inside] in the middle of [open] and [close]
/// if [inside] is not empty.
String _encloseIfNotEmpty(String open, String inside, String close) {
  if (inside == '') return '';
  return '$open$inside$close';
}

String _newLine({int depth = 0}) {
  return '\n${'  ' * depth}';
}

/// Merges two maps. For the same keys, their value lists will be concatenated.
///
/// ** After calling this, the original maps might get modified! **
Map<K, List<V>> _mergeMapValues<K, V>(Map<K, List<V>> a, Map<K, List<V>> b) {
  final merged = <K, List<V>>{};
  for (final key in {...a.keys, ...b.keys}) {
    if (!a.containsKey(key)) {
      merged[key] = b[key]!;
      continue;
    }
    if (!b.containsKey(key)) {
      merged[key] = a[key]!;
      continue;
    }

    // Merging the smaller one to the bigger one
    if (a[key]!.length > b[key]!.length) {
      merged[key] = a[key]!;
      merged[key]!.addAll(b[key]!);
    } else {
      merged[key] = b[key]!;
      merged[key]!.addAll(a[key]!);
    }
  }
  return merged;
}

/// **Naming Convention**
///
/// Let's take the following code as an example:
///
/// ```dart
/// Method definition
/// void f<T extends num, U>(JType<T> $T, JType<U> $U, T t, U u) {
///   // ...
/// }
/// f<int, String>($T, $U, t, u); // Calling the Method
/// ```
///
/// Here `f` will be replaced according to the place of usage.
///
/// * `fArgsDef` refers to `T t, U u` – the arguments in the method
///   definition.
/// * `fArgsCall` refer to `t, u` – the arguments passed to call the method.
/// * `fTypeParamsDef` refers to `<T extends num, U>` – the type parameters
///   of the method at the point of definition.
/// * `fTypeParamsCall` refers to `<int, String>` – the type parameters when
///   calling, or whenever we don't want to have the `extends` keyword.
/// * `fTypeClassesDef` refers to `JType<T> $T, JType<U> $U`.
/// * `fTypeClassesCall` refers to `$T, $U` when calling the method.
class DartGenerator extends Visitor<Classes, Future<void>> {
  final Config config;

  DartGenerator(this.config);

  static const cInitImport = 'import "dart:ffi" as ffi;\n'
      'import "package:jni/internal_helpers_for_jnigen.dart";\n';

  /// Initialization code for C based bindings.
  ///
  /// Should be called once in a package. In package-structured bindings
  /// this is placed in _init.dart in package root.
  String get cInitCode => '''
// Auto-generated initialization code.

final $_ffi.Pointer<T> Function<T extends $_ffi.NativeType>(String sym) $_lookup =
    $_protectedExtension.initGeneratedLibrary("${config.outputConfig.cConfig!.libraryName}");


''';

  static const autoGeneratedNotice = '// Autogenerated by jnigen. '
      'DO NOT EDIT!\n\n';
  static const defaultImports = '''
import "dart:isolate" show ReceivePort;
import "dart:ffi" as ffi;
import "package:jni/internal_helpers_for_jnigen.dart";
import "package:jni/jni.dart" as jni;

''';

  // Sort alphabetically.
  static const defaultLintSuppressions = '''
// ignore_for_file: annotate_overrides
// ignore_for_file: camel_case_extensions
// ignore_for_file: camel_case_types
// ignore_for_file: constant_identifier_names
// ignore_for_file: doc_directive_unknown
// ignore_for_file: file_names
// ignore_for_file: lines_longer_than_80_chars
// ignore_for_file: no_leading_underscores_for_local_identifiers
// ignore_for_file: non_constant_identifier_names
// ignore_for_file: overridden_fields
// ignore_for_file: unnecessary_cast
// ignore_for_file: unused_element
// ignore_for_file: unused_field
// ignore_for_file: unused_import
// ignore_for_file: unused_local_variable
// ignore_for_file: unused_shown_name

''';
  static const preImportBoilerplate =
      autoGeneratedNotice + defaultLintSuppressions + defaultImports;

  /// Run dart format command on [path].
  Future<void> _runDartFormat(String path) async {
    log.info('Running dart format...');
    final formatRes = await Process.run('dart', ['format', path]);
    // if negative exit code, likely due to an interrupt.
    if (formatRes.exitCode > 0) {
      log.fatal('Dart format completed with exit code ${formatRes.exitCode} '
          'This usually means there\'s a syntax error in bindings.\n'
          'Please look at the generated files and report a bug: \n'
          'https://github.com/dart-lang/native/issues/new?labels=package%3Ajnigen\n');
    }
  }

  @override
  Future<void> visit(Classes node) async {
    final cBased = config.outputConfig.bindingsType == BindingsType.cBased;
    final root = config.outputConfig.dartConfig.path;
    final preamble = config.preamble ?? '';
    if (config.outputConfig.dartConfig.structure ==
        OutputStructure.singleFile) {
      final file = File.fromUri(root);
      await file.create(recursive: true);
      log.info("Generating ${cBased ? "C + Dart" : "Pure Dart"} Bindings");
      final s = file.openWrite();
      s.writeln(preamble);
      s.writeln(autoGeneratedNotice);
      s.writeln(defaultLintSuppressions);
      s.writeln(defaultImports);
      if (cBased) {
        s.writeln(cInitCode);
      }
      final resolver = Resolver(
        importedClasses: config.importedClasses,
        currentClass: null, // Single file mode.
        inputClassNames: node.decls.keys.toSet(),
      );
      final classGenerator = _ClassGenerator(config, s, resolver);
      for (final classDecl in node.decls.values) {
        classDecl.accept(classGenerator);
      }
      await s.close();
      await _runDartFormat(file.path);
      return;
    }
    final files = <String, List<ClassDecl>>{};
    final packages = <String, Set<String>>{};
    for (final classDecl in node.decls.values) {
      final fileClass = Resolver.getFileClassName(classDecl.binaryName);

      files.putIfAbsent(fileClass, () => <ClassDecl>[]);
      files[fileClass]!.add(classDecl);

      packages.putIfAbsent(classDecl.packageName, () => {});
      packages[classDecl.packageName]!.add(fileClass.split('.').last);
    }

    log.info("Using dart root = $root");
    const initFileName = '_init.dart';
    if (cBased) {
      final initFileUri = root.resolve(initFileName);
      final initFile = File.fromUri(initFileUri);
      await initFile.create(recursive: true);
      final initStream = initFile.openWrite();
      initStream.writeln(preamble);
      initStream.writeln(cInitImport);
      initStream.writeln(cInitCode);
      await initStream.close();
    }
    for (var fileClassName in files.keys) {
      final relativeFileName = '${fileClassName.replaceAll('.', '/')}.dart';
      final dartFileUri = root.resolve(relativeFileName);
      final dartFile = await File.fromUri(dartFileUri).create(recursive: true);
      log.fine('$fileClassName -> ${dartFile.path}');

      final classesInFile = files[fileClassName]!;
      final dartFileStream = dartFile.openWrite();
      dartFileStream.writeln(preamble);
      dartFileStream.writeln(autoGeneratedNotice);
      dartFileStream.writeln(defaultLintSuppressions);
      dartFileStream.writeln(defaultImports);
      final s = StringBuffer();
      if (cBased) {
        final initFilePath = ('../' *
                relativeFileName.codeUnits
                    .where((cu) => cu == '/'.codeUnitAt(0))
                    .length) +
            initFileName;
        s.write('import "$initFilePath";');
      }
      final resolver = Resolver(
        importedClasses: config.importedClasses,
        currentClass: fileClassName,
        inputClassNames: node.decls.keys.toSet(),
      );
      final classGenerator = _ClassGenerator(config, s, resolver);
      for (final classDecl in classesInFile) {
        classDecl.accept(classGenerator);
      }
      dartFileStream.writeAll(resolver.getImportStrings(), '\n');
      dartFileStream.writeln(s.toString());
      await dartFileStream.close();
    }

    // write _package.dart export files
    for (var package in packages.keys) {
      final dirUri = root.resolve('${package.replaceAll('.', '/')}/');
      final exportFileUri = dirUri.resolve("_package.dart");
      final exportFile = File.fromUri(exportFileUri);
      exportFile.createSync(recursive: true);
      final exports =
          packages[package]!.map((cls) => 'export "$cls.dart";').join('\n');
      exportFile.writeAsStringSync(exports);
    }
    await _runDartFormat(root.toFilePath());
    log.info('Completed.');
  }
}

/// Generates the Dart class definition, type class, and the array extension.
class _ClassGenerator extends Visitor<ClassDecl, void> {
  final Config config;
  final StringSink s;
  final Resolver resolver;

  _ClassGenerator(
    this.config,
    this.s,
    this.resolver,
  );

  static const staticTypeGetter = 'type';
  static const instanceTypeGetter = '\$$staticTypeGetter';

  @override
  void visit(ClassDecl node) {
    final isDartOnly =
        config.outputConfig.bindingsType == BindingsType.dartOnly;

    // Docs.
    s.write('/// from: ${node.binaryName}\n');
    node.javadoc?.accept(_DocGenerator(s, depth: 0));

    // Class definition.
    final name = node.finalName;
    final superName = node.superclass!.accept(_TypeGenerator(resolver));
    final implClassName = '\$${name}Impl';
    final typeParamsDef = _encloseIfNotEmpty(
      '<',
      node.allTypeParams
          .accept(const _TypeParamGenerator(withExtends: true))
          .join(', '),
      '>',
    );
    final typeParams = node.allTypeParams
        .accept(const _TypeParamGenerator(withExtends: false));
    final typeParamsCall = _encloseIfNotEmpty(
      '<',
      typeParams.map((typeParam) => '$_typeParamPrefix$typeParam').join(', '),
      '>',
    );
    final staticTypeGetterCallArgs = _encloseIfNotEmpty(
      '(',
      typeParams.join(', '),
      ')',
    );
    final typeClassesDef = typeParams
        .map((typeParam) =>
            'final $_jType<$_typeParamPrefix$typeParam> $typeParam;')
        .join(_newLine(depth: 1));
    final ctorTypeClassesDef = typeParams
        .map((typeParam) => 'this.$typeParam,')
        .join(_newLine(depth: 2));
    final superClass = (node.classDecl.superclass!.type as DeclaredType);
    final superTypeClassesCall = superClass.classDecl.isObject
        ? ''
        : superClass.params
            .accept(_TypeClassGenerator(resolver))
            .map((typeClass) => '${typeClass.name},')
            .join(_newLine(depth: 2));
    s.write('''
class $name$typeParamsDef extends $superName {
  @override
  late final $_jType<$name$typeParamsCall> $instanceTypeGetter = $staticTypeGetter$staticTypeGetterCallArgs;

  $typeClassesDef

  $name.fromReference(
    $ctorTypeClassesDef
    $_jReference reference,
  ): super.fromReference(
    $superTypeClassesCall
    reference
  );

''');

    if (isDartOnly) {
      final internalName = node.internalName;
      s.write('''
  static final $_classRef = $_jni.JClass.forName(r"$internalName");

''');
    }

    // Static TypeClass getter.
    s.writeln(
        '  /// The type which includes information such as the signature of this class.');
    final typeClassName = node.typeClassName;
    if (typeParams.isEmpty) {
      s.writeln('static const $staticTypeGetter = $typeClassName();');
    } else {
      final staticTypeGetterTypeClassesDef = typeParams
          .map(
              (typeParam) => '$_jType<$_typeParamPrefix$typeParam> $typeParam,')
          .join(_newLine(depth: 2));
      final typeClassesCall =
          typeParams.map((typeParam) => '$typeParam,').join(_newLine(depth: 3));
      s.write('''
  static $typeClassName$typeParamsCall $staticTypeGetter$typeParamsDef(
    $staticTypeGetterTypeClassesDef
  ) {
    return $typeClassName(
      $typeClassesCall
    );
  }

''');
    }

    // Fields and Methods
    final fieldGenerator = _FieldGenerator(config, resolver, s);
    for (final field in node.fields) {
      field.accept(fieldGenerator);
    }
    final methodGenerator = _MethodGenerator(config, resolver, s);
    for (final method in node.methods) {
      method.accept(methodGenerator);
    }

    // Experimental: Interface implementation.
    if (node.declKind == DeclKind.interfaceKind &&
        (config.experiments?.contains(Experiment.interfaceImplementation) ??
            false)) {
      s.write('''
  /// Maps a specific port to the implemented interface.
  static final Map<int, $implClassName> _\$impls = {};
''');
      s.write(r'''
  ReceivePort? _$p;

  static jni.JObjectPtr _$invoke(
    int port,
    jni.JObjectPtr descriptor,
    jni.JObjectPtr args,
  ) {
    return _$invokeMethod(
      port,
      $MethodInvocation.fromAddresses(
        0,
        descriptor.address,
        args.address,
      ),
    );
  }

  static final ffi.Pointer<
          ffi.NativeFunction<
              jni.JObjectPtr Function(
                  ffi.Uint64, jni.JObjectPtr, jni.JObjectPtr)>>
      _$invokePointer = ffi.Pointer.fromFunction(_$invoke);

  static ffi.Pointer<ffi.Void> _$invokeMethod(
    int $p,
    $MethodInvocation $i,
  ) {
    try {
      final $d = $i.methodDescriptor.toDartString(releaseOriginal: true);
      final $a = $i.args;
    ''');
      final proxyMethodIf = _InterfaceMethodIf(resolver, s);
      for (final method in node.methods) {
        method.accept(proxyMethodIf);
      }
      s.write('''
    } catch (e) {
      return $_protectedExtension.newDartException(e.toString());
    }
    return jni.nullptr;
  }

  factory $name.implement(
    $implClassName$typeParamsCall \$impl,
  ) {
''');
      final typeClassesCall = typeParams
          .map((typeParam) => '\$impl.$typeParam,')
          .join(_newLine(depth: 3));
      s.write('''
    final \$p = ReceivePort();
    final \$x = $name.fromReference(
      $typeClassesCall
      $_protectedExtension.newPortProxy(
        r"${node.binaryName}",
        \$p,
        _\$invokePointer,
      ),
    ).._\$p = \$p;
    final \$a = \$p.sendPort.nativePort; 
    _\$impls[\$a] = \$impl;
    \$p.listen((\$m) {
      if (\$m == null) {
        _\$impls.remove(\$p.sendPort.nativePort);
        \$p.close();
        return;
      }
      final \$i = \$MethodInvocation.fromMessage(\$m as List<dynamic>);
      final \$r = _\$invokeMethod(\$p.sendPort.nativePort, \$i);
      $_protectedExtension.returnResult(\$i.result, \$r);
    });
    return \$x;
  }
  ''');
    }

    // Writing any custom code provided for this class.
    if (config.customClassBody?.containsKey(node.binaryName) ?? false) {
      s.writeln(config.customClassBody![node.binaryName]);
    }

    // End of Class definition.
    s.writeln('}');

    // Experimental: Interface implementation
    // Abstract and concrete Impl class definition.
    // Used for interface implementation.
    if (node.declKind == DeclKind.interfaceKind &&
        (config.experiments?.contains(Experiment.interfaceImplementation) ??
            false)) {
      // Abstract Impl class.
      final typeClassGetters = typeParams
          .map((typeParam) =>
              '$_jType<$_typeParamPrefix$typeParam> get $typeParam;')
          .join(_newLine(depth: 1));
      final abstractFactoryArgs = _encloseIfNotEmpty(
        '{',
        [
          ...typeParams
              .map((typeParam) => 'required $_jType<\$$typeParam> $typeParam,'),
          ...node.methods.accept(_ConcreteImplClosureCtorArg(resolver)),
        ].join(_newLine(depth: 2)),
        '}',
      );
      s.write('''
abstract interface class $implClassName$typeParamsDef {
  factory $implClassName(
    $abstractFactoryArgs
  ) = _$implClassName;

  $typeClassGetters

''');
      final abstractImplMethod = _AbstractImplMethod(resolver, s);
      for (final method in node.methods) {
        method.accept(abstractImplMethod);
      }
      s.writeln('}');

      // Concrete Impl class.
      // This is for passing closures instead of implementing the class.
      final concreteCtorArgs = _encloseIfNotEmpty(
        '{',
        [
          ...typeParams.map((typeParam) => 'required this.$typeParam,'),
          ...node.methods.accept(_ConcreteImplClosureCtorArg(resolver)),
        ].join(_newLine(depth: 2)),
        '}',
      );
      final setClosures = _encloseIfNotEmpty(
        ' :  ',
        node.methods
            .map((method) => '_${method.finalName} = ${method.finalName}')
            .join(', '),
        '',
      );
      final typeClassesDef = typeParams.map((typeParam) => '''
@override
final $_jType<\$$typeParam> $typeParam;
''').join(_newLine(depth: 1));
      s.write('''

class _$implClassName$typeParamsDef implements $implClassName$typeParamsCall {
  _$implClassName(
    $concreteCtorArgs
  )$setClosures;

  $typeClassesDef

''');
      final concreteClosureDef = _ConcreteImplClosureDef(resolver, s);
      for (final method in node.methods) {
        method.accept(concreteClosureDef);
      }
      s.writeln();
      final concreteMethodDef = _ConcreteImplMethod(resolver, s);
      for (final method in node.methods) {
        method.accept(concreteMethodDef);
      }
      s.writeln('}');
    }
    // TypeClass definition.
    final typeClassesCall =
        typeParams.map((typeParam) => '$typeParam,').join(_newLine(depth: 2));
    final signature = node.signature;
    final superTypeClass = superClass.accept(_TypeClassGenerator(resolver));
    final hashCodeTypeClasses = typeParams.join(', ');
    final equalityTypeClasses = typeParams
        .map((typeParam) => ' &&\n        $typeParam == other.$typeParam')
        .join();
    final hashCode = typeParams.isEmpty
        ? '($typeClassName).hashCode'
        : 'Object.hash($typeClassName, $hashCodeTypeClasses)';
    s.write('''
final class $typeClassName$typeParamsDef extends $_jType<$name$typeParamsCall> {
  $typeClassesDef

  const $typeClassName(
    $ctorTypeClassesDef
  );

  @override
  String get signature => r"$signature";

  @override
  $name$typeParamsCall fromReference($_jReference reference) => $name.fromReference(
    $typeClassesCall
    reference
  );

  @override
  $_jType get superType => ${superTypeClass.name};

  @override
  final superCount = ${node.superCount};

  @override
  int get hashCode => $hashCode;

  @override
  bool operator ==(Object other) {
    return other.runtimeType == ($typeClassName$typeParamsCall) &&
        other is $typeClassName$typeParamsCall$equalityTypeClasses;
  }
}

''');
    log.finest('Generated bindings for class ${node.binaryName}');
  }
}

/// Generates the JavaDoc comments.
class _DocGenerator extends Visitor<JavaDocComment, void> {
  final StringSink s;
  final int depth;

  const _DocGenerator(this.s, {required this.depth});

  @override
  void visit(JavaDocComment node) {
    final link = RegExp('{@link ([^{}]+)}');
    final indent = '  ' * depth;
    final comments = node.comment
        .replaceAllMapped(link, (match) => match.group(1) ?? '')
        .replaceAll('#', '\\#')
        .replaceAll('<p>', '')
        .replaceAll('</p>', '\n')
        .replaceAll('<b>', '__')
        .replaceAll('</b>', '__')
        .replaceAll('<em>', '_')
        .replaceAll('</em>', '_')
        .split('\n')
        .join('\n$indent///');
    s.write('''
$indent///
$indent/// $comments
''');
  }
}

/// Generates the user-facing Dart type.
class _TypeGenerator extends TypeVisitor<String> {
  final Resolver? resolver;

  const _TypeGenerator(this.resolver);

  @override
  String visitArrayType(ArrayType node) {
    final innerType = node.type;
    if (innerType.kind == Kind.primitive) {
      return '$_jArray<$_jni.j${(innerType.type as PrimitiveType).name}>';
    }
    return '$_jArray<${innerType.accept(this)}>';
  }

  @override
  String visitDeclaredType(DeclaredType node) {
    if (node.classDecl.binaryName == 'java.lang.Object' ||
        node.classDecl.binaryName == 'java.lang.String') {
      return '$_jni.${node.classDecl.finalName}';
    }

    // All type parameters of this type
    final allTypeParams = node.classDecl.allTypeParams
        .accept(const _TypeParamGenerator(withExtends: false))
        .toList();
    // The ones that are declared.
    final definedTypeParams = node.params.accept(this).toList();

    // Replacing the declared ones. They come at the end.
    // The rest will be JObject.
    if (allTypeParams.length >= node.params.length) {
      allTypeParams.replaceRange(
        0,
        allTypeParams.length - node.params.length,
        List.filled(
          allTypeParams.length - node.params.length,
          _jObject,
        ),
      );
      allTypeParams.replaceRange(
        allTypeParams.length - node.params.length,
        allTypeParams.length,
        definedTypeParams,
      );
    }

    final typeParams = _encloseIfNotEmpty('<', allTypeParams.join(', '), '>');
    final prefix = resolver?.resolvePrefix(node.classDecl) ?? '';
    return '$prefix${node.classDecl.finalName}$typeParams';
  }

  @override
  String visitPrimitiveType(PrimitiveType node) {
    return node.dartType;
  }

  @override
  String visitTypeVar(TypeVar node) {
    return '$_typeParamPrefix${node.name}';
  }

  @override
  String visitWildcard(Wildcard node) {
    // TODO(#141): Support wildcards
    return super.visitWildcard(node);
  }

  @override
  String visitNonPrimitiveType(ReferredType node) {
    return _jObject;
  }
}

class _TypeClass {
  final String name;
  final bool canBeConst;

  const _TypeClass(this.name, this.canBeConst);
}

/// Generates the type class.
class _TypeClassGenerator extends TypeVisitor<_TypeClass> {
  final bool isConst;

  /// Whether or not to return the equivalent boxed type class for primitives.
  /// Only for interface implemetation.
  final bool boxPrimitives;

  /// Whether or not to find the correct type variable from the static map.
  /// Only for interface implemetation.
  final bool typeVarFromMap;

  final Resolver resolver;

  _TypeClassGenerator(
    this.resolver, {
    this.isConst = true,
    this.boxPrimitives = false,
    this.typeVarFromMap = false,
  });

  @override
  _TypeClass visitArrayType(ArrayType node) {
    final innerTypeClass = node.type.accept(_TypeClassGenerator(
      resolver,
      isConst: false,
      boxPrimitives: false,
      typeVarFromMap: typeVarFromMap,
    ));
    final ifConst = innerTypeClass.canBeConst && isConst ? 'const ' : '';
    return _TypeClass(
      '$ifConst${_jArray}Type(${innerTypeClass.name})',
      innerTypeClass.canBeConst,
    );
  }

  @override
  _TypeClass visitDeclaredType(DeclaredType node) {
    final allTypeParams = node.classDecl.allTypeParams
        .accept(const _TypeParamGenerator(withExtends: false))
        .toList();

    // The ones that are declared.
    final definedTypeClasses = node.params.accept(_TypeClassGenerator(
      resolver,
      isConst: false,
      boxPrimitives: false,
      typeVarFromMap: typeVarFromMap,
    ));

    // Can be const if all the type parameters are defined and each of them are
    // also const.
    final canBeConst =
        allTypeParams.isEmpty || definedTypeClasses.every((e) => e.canBeConst);

    // Replacing the declared ones. They come at the end.
    // The rest will be `JObjectType`.
    if (allTypeParams.length >= node.params.length) {
      allTypeParams.replaceRange(
        0,
        allTypeParams.length - node.params.length,
        List.filled(
          allTypeParams.length - node.params.length,
          // Adding const to subexpressions if the entire expression is not const.
          '${canBeConst ? '' : 'const '}${_jObject}Type()',
        ),
      );
      allTypeParams.replaceRange(
        allTypeParams.length - node.params.length,
        allTypeParams.length,
        // Adding const to subexpressions if the entire expression is not const.
        definedTypeClasses.map((param) =>
            '${param.canBeConst && !canBeConst ? 'const ' : ''}${param.name}'),
      );
    }

    final args = allTypeParams.join(', ');
    final ifConst = isConst && canBeConst ? 'const ' : '';
    final prefix = resolver.resolvePrefix(node.classDecl);
    return _TypeClass(
      '$ifConst$prefix${node.classDecl.typeClassName}($args)',
      canBeConst,
    );
  }

  @override
  _TypeClass visitPrimitiveType(PrimitiveType node) {
    final ifConst = isConst ? 'const ' : '';
    final name = boxPrimitives ? 'J${node.boxedName}' : 'j${node.name}';
    return _TypeClass('$ifConst$_jni.${name}Type()', true);
  }

  @override
  _TypeClass visitTypeVar(TypeVar node) {
    if (typeVarFromMap) {
      return _TypeClass('_\$impls[\$p]!.${node.name}', false);
    }
    return _TypeClass(node.name, false);
  }

  @override
  _TypeClass visitWildcard(Wildcard node) {
    // TODO(#141): Support wildcards
    return super.visitWildcard(node);
  }

  @override
  _TypeClass visitNonPrimitiveType(ReferredType node) {
    final ifConst = isConst ? 'const ' : '';
    return _TypeClass('$ifConst${_jObject}Type()', true);
  }
}

class _TypeParamGenerator extends Visitor<TypeParam, String> {
  final bool withExtends;

  const _TypeParamGenerator({required this.withExtends});

  @override
  String visit(TypeParam node) {
    if (!withExtends) {
      return node.name;
    }
    // TODO(#144): resolve the actual type being extended, if any.
    return '$_typeParamPrefix${node.name} extends $_jObject';
  }
}

class _JniResultGetter extends TypeVisitor<String> {
  final Resolver resolver;

  _JniResultGetter(this.resolver);

  @override
  String visitPrimitiveType(PrimitiveType node) {
    if (node.name == 'void') return 'check()';
    if (node.name == 'double') return 'doubleFloat';
    if (node.name == 'int') return 'integer';
    return node.name;
  }

  @override
  String visitNonPrimitiveType(ReferredType node) {
    final type = node.accept(_TypeClassGenerator(resolver)).name;
    return 'object($type)';
  }
}

/// Type signature for C-based bindings.
///
/// When `isFFi` is `true`, it generates the ffi type signature and when it's
/// false, it generates the dart type signature.
///
/// For example `ffi.Int32` is an ffi type signature while `int` is a Dart one:
/// ```dart
/// jniLookup<ffi.NativeFunction<jni.JniResult Function(ffi.Int32, ffi.Int32)>>(
///       "sum")
///   .asFunction<jni.JniResult Function(int, int)>();
/// ```
class _TypeSig extends TypeVisitor<String> {
  final bool isFfi;

  const _TypeSig({required this.isFfi});

  @override
  String visitPrimitiveType(PrimitiveType node) {
    if (isFfi) return '$_ffi.${node.ffiType}';
    if (node.name == 'boolean') return 'int';
    return node.dartType;
  }

  @override
  String visitNonPrimitiveType(ReferredType node) {
    return _voidPointer;
  }
}

class _ToNativeSuffix extends TypeVisitor<String> {
  const _ToNativeSuffix();

  @override
  String visitPrimitiveType(PrimitiveType node) {
    if (node.name == 'boolean') {
      return ' ? 1 : 0';
    }
    return '';
  }

  @override
  String visitNonPrimitiveType(ReferredType node) {
    return '.$_selfPointer';
  }
}

class _FieldGenerator extends Visitor<Field, void> {
  final Config config;
  final Resolver resolver;
  final StringSink s;

  const _FieldGenerator(this.config, this.resolver, this.s);

  void writeCAccessor(Field node) {
    final name = node.finalName;
    final cName = node.accept(const CFieldName());
    final ifRef = node.isStatic ? '' : '$_jPointer, ';

    s.write('''
  static final _get_$name =
      $_lookup<$_ffi.NativeFunction<$_jResult Function($ifRef)>>(
              "get_$cName")
          .asFunction<$_jResult Function($ifRef)>();

''');

    if (!node.isFinal) {
      final ffiSig = node.type.accept(const _TypeSig(isFfi: true));
      final dartSig = node.type.accept(const _TypeSig(isFfi: false));
      s.write('''
  static final _set_$name =
      $_lookup<$_ffi.NativeFunction<$_jResult Function($ifRef$ffiSig)>>(
              "set_$cName")
          .asFunction<$_jResult Function($ifRef$dartSig)>();

''');
    }
  }

  String cGetter(Field node) {
    final name = node.finalName;
    final getter = node.type.accept(_JniResultGetter(resolver));
    final self = node.isStatic ? '' : _selfPointer;
    return '_get_$name($self).$getter';
  }

  String cSetter(Field node) {
    final name = node.finalName;
    final self = node.isStatic ? '' : '$_selfPointer, ';
    final toNativeSuffix = node.type.accept(const _ToNativeSuffix());
    return '_set_$name(${self}value$toNativeSuffix).check()';
  }

  void writeDartOnlyAccessor(Field node) {
    final name = node.finalName;
    final staticOrInstance = node.isStatic ? 'static' : 'instance';
    final descriptor = node.type.descriptor;
    s.write('''
  static final _id_$name =
      $_classRef.${staticOrInstance}FieldId(
        r"${node.name}",
        r"$descriptor",
      );
''');
  }

  String dartOnlyGetter(Field node) {
    final name = node.finalName;
    final self = node.isStatic ? _classRef : _self;
    final type = node.type.accept(_TypeClassGenerator(resolver)).name;
    return '_id_$name.get($self, $type)';
  }

  String dartOnlySetter(Field node) {
    final name = node.finalName;
    final self = node.isStatic ? _classRef : _self;
    final type = node.type.accept(_TypeClassGenerator(resolver)).name;
    return '_id_$name.set($self, $type, value)';
  }

  void writeDocs(Field node, {required bool writeReleaseInstructions}) {
    final originalDecl = '${node.type.shorthand} ${node.name}';
    s.writeln('  /// from: ${node.modifiers.join(' ')} $originalDecl');
    if (node.type.kind != Kind.primitive && writeReleaseInstructions) {
      s.writeln(_releaseInstruction);
    }
    node.javadoc?.accept(_DocGenerator(s, depth: 1));
  }

  @override
  void visit(Field node) {
    final isCBased = config.outputConfig.bindingsType == BindingsType.cBased;

    // Check if it should be a `static const` getter.
    if (node.isFinal && node.isStatic && node.defaultValue != null) {
      final name = node.finalName;
      final value = node.defaultValue!;
      if (value is num || value is bool) {
        writeDocs(node, writeReleaseInstructions: false);
        s.writeln('  static const $name = $value;');
        return;
      }
    }

    // Accessors.
    (isCBased ? writeCAccessor : writeDartOnlyAccessor)(node);

    // Getter docs.
    writeDocs(node, writeReleaseInstructions: true);

    final name = node.finalName;
    final ifStatic = node.isStatic ? 'static ' : '';
    final type = node.type.accept(_TypeGenerator(resolver));
    s.write('$ifStatic$type get $name => ');
    s.write((isCBased ? cGetter : dartOnlyGetter)(node));
    s.writeln(';\n');
    if (!node.isFinal) {
      // Setter docs.
      writeDocs(node, writeReleaseInstructions: true);

      s.write('${ifStatic}set $name($type value) => ');
      s.write((isCBased ? cSetter : dartOnlySetter)(node));
      s.writeln(';\n');
    }
  }
}

class _MethodTypeSig extends Visitor<Method, String> {
  final bool isFfi;

  const _MethodTypeSig({required this.isFfi});

  @override
  String visit(Method node) {
    final args = [
      if (!node.isCtor && !node.isStatic) _voidPointer,
      ...node.params.map((param) => param.type).accept(
            _TypeSig(isFfi: isFfi),
          )
    ].join(', ');
    return '$_jResult Function($args)';
  }
}

/// Generates Dart bindings for Java methods.
class _MethodGenerator extends Visitor<Method, void> {
  final Config config;
  final Resolver resolver;
  final StringSink s;

  const _MethodGenerator(this.config, this.resolver, this.s);

  void writeCAccessor(Method node) {
    final name = node.finalName;
    final cName = node.accept(const CMethodName());
    final ffiSig = node.accept(const _MethodTypeSig(isFfi: true));
    final dartSig = node.accept(const _MethodTypeSig(isFfi: false));
    s.write('''
  static final _$name =
          $_lookup<$_ffi.NativeFunction<$ffiSig>>("$cName")
      .asFunction<$dartSig>();

''');
  }

  void writeDartOnlyAccessor(Method node) {
    final name = node.finalName;
    final kind = node.isCtor
        ? 'constructor'
        : node.isStatic
            ? 'staticMethod'
            : 'instanceMethod';
    final descriptor = node.descriptor;
    s.write('''
  static final _id_$name = $_classRef.${kind}Id(
''');
    if (!node.isCtor) s.writeln('    r"${node.name}",');
    s.write('''
    r"$descriptor",
  );
''');
  }

  bool isSuspendFun(Method node) {
    return node.asyncReturnType != null;
  }

  String cCtor(Method node) {
    final name = node.finalName;
    final params =
        node.params.accept(const _ParamCall(isCBased: true)).join(', ');
    return '_$name($params).reference';
  }

  String dartOnlyCtor(Method node) {
    final name = node.finalName;
    final params =
        node.params.accept(const _ParamCall(isCBased: false)).join(', ');
    return '_id_$name($_classRef, referenceType, [$params])';
  }

  String cMethodCall(Method node) {
    final name = node.finalName;
    final params = [
      if (!node.isStatic) _selfPointer,
      ...node.params.accept(const _ParamCall(isCBased: true)),
    ].join(', ');
    final resultGetter = node.returnType.accept(_JniResultGetter(resolver));
    return '_$name($params).$resultGetter';
  }

  String dartOnlyMethodCall(Method node) {
    final name = node.finalName;
    final self = node.isStatic ? _classRef : _self;
    final params =
        node.params.accept(const _ParamCall(isCBased: false)).join(', ');
    final type = node.returnType.accept(_TypeClassGenerator(resolver)).name;
    return '_id_$name($self, $type, [$params])';
  }

  @override
  void visit(Method node) {
    final isCBased = config.outputConfig.bindingsType == BindingsType.cBased;

    // Accessors
    (isCBased ? writeCAccessor : writeDartOnlyAccessor)(node);

    // Docs
    s.write('  /// from: ');
    s.writeAll(node.modifiers.map((m) => '$m '));
    s.write('${node.returnType.shorthand} ${node.name}(');
    s.writeAll(node.params.map((p) => '${p.type.shorthand} ${p.name}'), ', ');
    s.writeln(')');
    if (node.returnType.kind != Kind.primitive || node.isCtor) {
      s.writeln(_releaseInstruction);
    }
    node.javadoc?.accept(_DocGenerator(s, depth: 1));

    // Used for inferring the type parameter from the given parameters.
    final typeLocators = node.params
        .accept(_ParamTypeLocator(resolver: resolver))
        .fold(<String, List<String>>{}, _mergeMapValues).map(
            (key, value) => MapEntry(
                  key,
                  _encloseIfNotEmpty(
                    '[',
                    value.delimited(', '),
                    ']',
                  ),
                ));

    bool isRequired(TypeParam typeParam) {
      return (typeLocators[typeParam.name] ?? '').isEmpty;
    }

    final typeInference =
        (node.isCtor ? node.classDecl.allTypeParams : node.typeParams)
            .where((tp) => !isRequired(tp))
            .map((tp) => tp.name)
            .map((tp) => '$tp ??= $_jni.lowestCommonSuperType'
                '(${typeLocators[tp]}) as $_jType<$_typeParamPrefix$tp>;')
            .join(_newLine(depth: 2));

    if (node.isCtor) {
      final className = node.classDecl.finalName;
      final name = node.finalName;
      final ctorName = name == 'new0' ? className : '$className.$name';
      final paramsDef = node.params.accept(_ParamDef(resolver)).delimited(', ');
      final typeClassDef = _encloseIfNotEmpty(
        '{',
        node.classDecl.allTypeParams
            .map((typeParam) => typeParam
                .accept(_CtorTypeClassDef(isRequired: isRequired(typeParam))))
            .delimited(', '),
        '}',
      );
      final typeClassCall = node.classDecl.allTypeParams
          .map((typeParam) =>
              typeParam.accept(const _TypeParamGenerator(withExtends: false)))
          .delimited(', ');

      final ctorExpr = (isCBased ? cCtor : dartOnlyCtor)(node);
      s.write('''
  factory $ctorName($paramsDef$typeClassDef) {
    $typeInference
    return ${node.classDecl.finalName}.fromReference(
      $typeClassCall
      $ctorExpr
    );
  }

''');
      return;
    }

    final name = node.finalName;
    final returnType = isSuspendFun(node)
        ? 'Future<${node.asyncReturnType!.accept(_TypeGenerator(resolver))}>'
        : node.returnType.accept(_TypeGenerator(resolver));
    final returnTypeClass = (node.asyncReturnType ?? node.returnType)
        .accept(_TypeClassGenerator(resolver))
        .name;
    final ifStatic = node.isStatic ? 'static ' : '';
    final defArgs = node.params.accept(_ParamDef(resolver)).toList();
    final typeClassDef = _encloseIfNotEmpty(
      '{',
      node.typeParams
          .map((typeParam) => typeParam.accept(_MethodTypeClassDef(
                isRequired: isRequired(typeParam),
              )))
          .delimited(', '),
      '}',
    );
    final typeParamsDef = _encloseIfNotEmpty(
      '<',
      node.typeParams
          .accept(const _TypeParamGenerator(withExtends: true))
          .join(', '),
      '>',
    );
    if (isSuspendFun(node)) {
      defArgs.removeLast();
    }
    final params = defArgs.delimited(', ');
    s.write('  $ifStatic$returnType $name$typeParamsDef($params$typeClassDef)');
    final callExpr = (isCBased ? cMethodCall : dartOnlyMethodCall)(node);
    if (isSuspendFun(node)) {
      final returningType =
          node.asyncReturnType!.accept(_TypeClassGenerator(resolver)).name;
      s.write('''async {
    $typeInference
    final \$p = ReceivePort();
    final \$c = $_jObject.fromReference($_protectedExtension.newPortContinuation(\$p));
    $callExpr;
    final \$o = $_jGlobalReference($_jPointer.fromAddress(await \$p.first));
    final \$k = $returnTypeClass.jClass.reference.pointer;
    if (!$_jni.Jni.env.IsInstanceOf(\$o.pointer, \$k)) {
      throw "Failed";
    }
    return $returningType.fromReference(\$o);
  }

''');
    } else {
      final returning = returnType == 'void' ? callExpr : 'return $callExpr';
      s.writeln('''{
    $typeInference
    $returning;
  }
''');
    }
  }
}

/// Generates the method type param definition.
///
/// For example `required JObjType<T> $T` in:
/// ```dart
/// void bar(..., {required JObjType<T> $T}) => ...
/// ```
class _MethodTypeClassDef extends Visitor<TypeParam, String> {
  final bool isRequired;

  const _MethodTypeClassDef({required this.isRequired});

  @override
  String visit(TypeParam node) {
    return '${isRequired ? 'required ' : ''}$_jType'
        '<$_typeParamPrefix${node.name}>${isRequired ? '' : '?'} ${node.name}';
  }
}

/// Generates the class type param definition. Used only in constructors.
///
/// For example `required this.$T` in:
/// ```dart
/// class Foo {
///   final JObjType<T> $T;
///   Foo(..., {required this.$T}) => ...
/// }
/// ```
class _CtorTypeClassDef extends Visitor<TypeParam, String> {
  final bool isRequired;

  const _CtorTypeClassDef({required this.isRequired});

  @override
  String visit(TypeParam node) {
    return '${isRequired ? 'required ' : ''} $_jType'
        '<$_typeParamPrefix${node.name}>${isRequired ? '' : '?'} ${node.name}';
  }
}

/// Method parameter's definition.
///
/// For example `Foo foo` in:
/// ```dart
/// void bar(Foo foo) => ...
/// ```
class _ParamDef extends Visitor<Param, String> {
  final Resolver resolver;

  const _ParamDef(this.resolver);

  @override
  String visit(Param node) {
    final type = node.type.accept(_TypeGenerator(resolver));
    return '$type ${node.finalName}';
  }
}

/// Method parameter used in calling the native method.
///
/// For example `foo.reference.pointer` in:
/// ```dart
/// void bar(Foo foo) => _bar(foo.reference.pointer);
/// ```
class _ParamCall extends Visitor<Param, String> {
  final bool isCBased;

  const _ParamCall({required this.isCBased});

  @override
  String visit(Param node) {
    final nativeSuffix = node.type.accept(const _ToNativeSuffix());
    final paramCall = '${node.finalName}$nativeSuffix';
    if (!isCBased) {
      // We need to wrap [paramCall] in the appropriate wrapper class.
      return node.type.accept(_JValueWrapper(paramCall));
    }
    return paramCall;
  }
}

/// Wraps the parameter in the appropriate JValue wrapper class.
///
/// For instance, `int` in Dart can be mapped to `long` or `int` or ... in Java.
/// The wrapper class is how we identify the type in pure dart bindings.
class _JValueWrapper extends TypeVisitor<String> {
  final String param;

  _JValueWrapper(this.param);

  @override
  String visitNonPrimitiveType(ReferredType node) {
    return param;
  }

  @override
  String visitPrimitiveType(PrimitiveType node) {
    if (node.name == 'long' ||
        node.name == 'double' ||
        node.name == 'boolean') {
      return param;
    }
    return '$_jni.JValue${node.name.capitalize()}($param)';
  }
}

/// A pair of [StringBuffer]s that can create an expression from the outer layer
/// inwards.
///
/// For example:
/// ```dart
/// final buffer = OutsideInBuffer(); // asterisk (*) is used to show the middle
/// buffer.appendLeft('f('); // f(*
/// buffer.prependRight('x)'); // f(*x)
/// buffer.appendLeft('g('); // f(g(*x)
/// buffer.prependRight('y) + '); // f(g(*y) + x)
/// buffer.toString(); // f(g(y) + x)
/// ```
@visibleForTesting
class OutsideInBuffer {
  final StringBuffer _leftBuffer;
  final StringBuffer _rightBuffer;

  OutsideInBuffer()
      : _leftBuffer = StringBuffer(),
        _rightBuffer = StringBuffer();

  void prependRight(Object? object) {
    final s = object.toString();
    for (var i = 0; i < s.length; ++i) {
      _rightBuffer.write(s[s.length - i - 1]);
    }
  }

  void appendLeft(Object? object) {
    _leftBuffer.write(object);
  }

  @override
  String toString() {
    return _leftBuffer.toString() + _rightBuffer.toString().reversed;
  }
}

/// The ways to locate each type parameter.
///
/// For example in `JArray<JMap<$T, $T>> a`, `T` can be retreived using
/// ```dart
/// ((((a.$type as jni.JArrayType).elementType) as $JMapType).K) as jni.JObjType<$T>
/// ```
/// and
/// ```dart
/// ((((a.$type as jni.JArrayType).elementType) as $JMapType).V) as jni.JObjType<$T>
/// ```
class _ParamTypeLocator extends Visitor<Param, Map<String, List<String>>> {
  final Resolver resolver;

  _ParamTypeLocator({required this.resolver});

  @override
  Map<String, List<String>> visit(Param node) {
    return node.type.accept(_TypeVarLocator(resolver: resolver)).map(
          (key, value) => MapEntry(
            key,
            value
                .map((e) =>
                    (e..appendLeft('${node.finalName}.\$type')).toString())
                .toList(),
          ),
        );
  }
}

class _TypeVarLocator extends TypeVisitor<Map<String, List<OutsideInBuffer>>> {
  final Resolver resolver;

  _TypeVarLocator({required this.resolver});

  @override
  Map<String, List<OutsideInBuffer>> visitNonPrimitiveType(ReferredType node) {
    return {};
  }

  @override
  Map<String, List<OutsideInBuffer>> visitWildcard(Wildcard node) {
    // TODO(#141): Support wildcards
    return super.visitWildcard(node);
  }

  @override
  Map<String, List<OutsideInBuffer>> visitTypeVar(TypeVar node) {
    return {
      node.name: [
        OutsideInBuffer(),
      ],
    };
  }

  @override
  Map<String, List<OutsideInBuffer>> visitDeclaredType(DeclaredType node) {
    if (node.classDecl.isObject) {
      return {};
    }
    final offset = node.classDecl.allTypeParams.length - node.params.length;
    final result = <String, List<OutsideInBuffer>>{};
    final prefix = resolver.resolvePrefix(node.classDecl);
    final typeClass = '$prefix${node.classDecl.typeClassName}';
    for (var i = 0; i < node.params.length; ++i) {
      final typeParam = node.classDecl.allTypeParams[i + offset].name;
      final exprs = node.params[i].accept(this);
      for (final expr in exprs.entries) {
        for (final buffer in expr.value) {
          buffer.appendLeft('(');
          buffer.prependRight(' as $typeClass).$typeParam');
          result[expr.key] = (result[expr.key] ?? [])..add(buffer);
        }
      }
    }
    return result;
  }

  @override
  Map<String, List<OutsideInBuffer>> visitArrayType(ArrayType node) {
    final exprs = node.type.accept(this);
    for (final e in exprs.values.expand((i) => i)) {
      e.appendLeft('((');
      e.prependRight(' as ${_jArray}Type).elementType as $_jType)');
    }
    return exprs;
  }

  @override
  Map<String, List<OutsideInBuffer>> visitPrimitiveType(PrimitiveType node) {
    return {};
  }
}

/// Method defintion for Impl abstract class used for interface implementation.
class _AbstractImplMethod extends Visitor<Method, void> {
  final Resolver resolver;
  final StringSink s;

  _AbstractImplMethod(this.resolver, this.s);

  @override
  void visit(Method node) {
    final returnType = node.returnType.accept(_TypeGenerator(resolver));
    final name = node.finalName;
    final args = node.params.accept(_ParamDef(resolver)).join(', ');
    s.writeln('  $returnType $name($args);');
  }
}

/// Closure defintion for concrete Impl class used for interface implementation.
class _ConcreteImplClosureDef extends Visitor<Method, void> {
  final Resolver resolver;
  final StringSink s;

  _ConcreteImplClosureDef(this.resolver, this.s);

  @override
  void visit(Method node) {
    final returnType = node.returnType.accept(_TypeGenerator(resolver));
    final name = node.finalName;
    final args = node.params.accept(_ParamDef(resolver)).join(', ');
    s.writeln('  final $returnType Function($args) _$name;');
  }
}

/// Closure argument for concrete Impl class constructor.
/// Used for interface implementation.
class _ConcreteImplClosureCtorArg extends Visitor<Method, String> {
  final Resolver resolver;

  _ConcreteImplClosureCtorArg(this.resolver);

  @override
  String visit(Method node) {
    final returnType = node.returnType.accept(_TypeGenerator(resolver));
    final name = node.finalName;
    final args = node.params.accept(_ParamDef(resolver)).join(', ');
    return 'required $returnType Function($args) $name,';
  }
}

/// Method defintion for concrete Impl class used for interface implementation.
class _ConcreteImplMethod extends Visitor<Method, void> {
  final Resolver resolver;
  final StringSink s;

  _ConcreteImplMethod(this.resolver, this.s);

  @override
  void visit(Method node) {
    final returnType = node.returnType.accept(_TypeGenerator(resolver));
    final name = node.finalName;
    final argsDef = node.params.accept(_ParamDef(resolver)).join(', ');
    final argsCall = node.params.map((param) => param.finalName).join(', ');
    s.write('''
  $returnType $name($argsDef) {
    return _$name($argsCall);
  }''');
  }
}

/// The if statement to check which method has been called from the proxy class.
class _InterfaceMethodIf extends Visitor<Method, void> {
  final Resolver resolver;
  final StringSink s;

  _InterfaceMethodIf(this.resolver, this.s);

  @override
  void visit(Method node) {
    final isVoid = node.returnType.name == 'void';
    final signature = node.javaSig;
    final saveResult = isVoid ? '' : 'final \$r = ';
    final name = node.finalName;
    s.write('''
        if (\$d == r"$signature") {
          ${saveResult}_\$impls[\$p]!.$name(
''');
    for (var i = 0; i < node.params.length; ++i) {
      node.params[i].accept(_InterfaceParamCast(resolver, s, paramIndex: i));
    }
    const returnBox = _InterfaceReturnBox();
    s.write('''
          );
          return ${node.returnType.accept(returnBox)};
        }
''');
  }
}

/// Generates casting to the correct parameter type from the list of JObject
/// arguments received from the call to the proxy class.
class _InterfaceParamCast extends Visitor<Param, void> {
  final Resolver resolver;
  final StringSink s;
  final int paramIndex;

  _InterfaceParamCast(
    this.resolver,
    this.s, {
    required this.paramIndex,
  });

  @override
  void visit(Param node) {
    final typeClass = node.type
        .accept(_TypeClassGenerator(
          resolver,
          boxPrimitives: true,
          typeVarFromMap: true,
        ))
        .name;
    s.write('\$a[$paramIndex].castTo($typeClass, releaseOriginal: true)');
    if (node.type.kind == Kind.primitive) {
      // Convert to Dart type.
      final name = (node.type.type as PrimitiveType).name;
      s.write('.${name}Value(releaseOriginal: true)');
    }
    s.writeln(',');
  }
}

/// Boxes the returned primitive value into the correct Boxed type.
/// Only returns the reference for non primitive types.
/// Returns null for void.
///
/// Since Dart doesn't know that this global reference is still used, it might
/// garbage collect it via [NativeFinalizer] thus making it invalid.
/// This passes the ownership to Java using [setAsReleased].
///
/// `toPointer` detaches the object from the [NativeFinalizer] and Java
/// will clean up the global reference afterwards.
///
/// For example `$r.toJInteger().reference.toPointer()` when the return
/// type is `integer`.
class _InterfaceReturnBox extends TypeVisitor<String> {
  const _InterfaceReturnBox();

  @override
  String visitNonPrimitiveType(ReferredType node) {
    // Casting is done to create a new global reference. The user might
    // use the original reference elsewhere and so the original object
    // should not be [setAsReleased].
    return '(\$r as $_jObject).castTo(const ${_jObject}Type()).reference.toPointer()';
  }

  @override
  String visitPrimitiveType(PrimitiveType node) {
    if (node.name == 'void') {
      return '$_jni.nullptr';
    }
    return '$_jni.J${node.boxedName}(\$r).reference.toPointer()';
  }
}
